
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Collections;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.IO;
using System.Xml;

using Swaf;
using Swaf.BizObj;
using Swaf.BizObj.basic;
using Swaf.Container;
using Swaf.Crypto;
using Swaf.Resource;
using Swaf.Xml;

using Syrinx;

namespace MediaCache
{
	/// <summary>
	/// Designed to be used as an XML Catalog entry class.  Each XML element in the catalog for
	/// this class specifies how the image cache should work and where its image store/cache 
	/// is located.
	/// </summary>
	/// <remarks>This class will manage the dynamic creation of images that are resized and
	/// adjusted based on an origional image in a directory specified to this class during 
	/// consturction.</remarks>
	public abstract class BaseImageCache : INameAddressSupport
	{
		protected IBizObj m_cacheOptions;
        protected IBizObj m_origCacheOptions;
		private ICryptoManager m_crypto;
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="req"></param>
        /// <param name="catalogName"></param>
		public BaseImageCache(LineInfoXmlElement req, Swaf.Container.XmlCatalogContainerNode.NameInfo catalogName)
		{
			IApplication app = Application.currentApp;
            IResourceManager resMgr = app.resMgr;
			m_cacheOptions = app.bizObjMgr.createSource(SourceType.BasicSource, req.OuterXml).getList("",null)[0];
            m_origCacheOptions = m_cacheOptions.Clone() as IBizObj;
		}
        
        protected string ExtensionName
        {
            get { return m_cacheOptions.get("extensionName").ToString(); }
            set { m_cacheOptions.put("extensionName", value); }
        }
        
        /// <summary>
        /// 
        /// </summary>
        public IBizObj CacheConfig { get { return m_origCacheOptions; } }

		public string CacheName
		{
			get
			{
				string s =  CacheConfig.get("baseCacheLocation").ToString();
				s= string.Format("{0}{1}",Application.currentApp.appData["MediaCacheNamePrefix"], s.Substring(s.LastIndexOf(Path.DirectorySeparatorChar)+1));
				return s;
			}
		}

		public string PreviewBaseUrl
		{
			get { return (string)m_cacheOptions.get("previewBaseUrl"); }
			set { m_cacheOptions.put("previewBaseUrl", value); }
		}
		/// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="scalingName"></param>
        /// <returns></returns>
		public IBizObj getImage(string name, string scalingName)
		{
			name = calcRelativeName(name, null);
			IBizObj sizing = null;
            IBizObj imgAdjInfo = null;
			if(scalingName != null)
			{
				sizing = getScalingOptions();
                if (sizing != null)
                    imgAdjInfo = sizing.get("adjustments." + scalingName) as IBizObj;

				if(isScaledImageOutdated(ref name, sizing, imgAdjInfo))
					createScaledImage(name, sizing, imgAdjInfo);
			}
			return retrieveImage(name, sizing, imgAdjInfo);
		}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="scalingName"></param>
        /// <returns></returns>
		public MediaStream getImageStream(string name, string scalingName)
		{
			name = calcRelativeName(name, null);
			IBizObj sizing = null;
            IBizObj imgAdjInfo = null;
            if (scalingName != null && name != null && name != "")
			{
				sizing = getScalingOptions();
                if(sizing != null)
                    imgAdjInfo = sizing.get("adjustments." + scalingName) as IBizObj;

				if(isScaledImageOutdated(ref name, sizing, imgAdjInfo))
					createScaledImage(name, sizing, imgAdjInfo);
			}
			return retrieveImageStream(name, sizing, imgAdjInfo);
		}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="scalingName"></param>
        /// <returns></returns>
		public Bitmap getImageBitmap(string name, string scalingName)
		{
			name = calcRelativeName(name, null); //Gets the .ext of image if not in name param
			IBizObj sizing = null;
            IBizObj imgAdjInfo = null;
            if (scalingName != null)
			{
				sizing = getScalingOptions();
                if (sizing != null)
                    imgAdjInfo = sizing.get("adjustments." + scalingName) as IBizObj;

				if(isScaledImageOutdated(ref name, sizing, imgAdjInfo))
					createScaledImage(name, sizing, imgAdjInfo);
			}
			return retrieveImageBitmap(name, sizing, imgAdjInfo);
		}
        
        /// <summary>
        /// 
        /// </summary>
		public IBizObjList Images
		{
			get
			{
				return getImages(ImageListType.filesOnly, null);
			}
		}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="imageFileName"></param>
        /// <returns></returns>
        public IBizObj getImageFileInfo(string imageFileName)
        {
            imageFileName = calcRelativeName(imageFileName, null);
            IBizObj bizObj = Application.currentApp.bizObjMgr.create("ImageProperties");
            MediaStream ms = retrieveImageStream(imageFileName, null,null);
            Image img = (Image)Bitmap.FromStream(ms.DataStream, false, false);

            if (img!=null)
            {
                EXIF exif = new EXIF(img);
                foreach (PropertyItem property in img.PropertyItems)
                {
                    string fldName = Enum.GetName(typeof(exifCode), property.Id);
                    if (fldName != null && bizObj.containsField(fldName))
                    {
                        exifCode code = (exifCode)Enum.Parse(typeof(exifCode), fldName);
                        bizObj.put(fldName, exif.GetEXIFProperty(code));
                    }
                }
            }
            ms.Dispose();
            return bizObj;

        }
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="listOf"></param>
        /// <param name="fileFilters"></param>
        /// <returns></returns>
		public abstract IBizObjList getImages(ImageListType listOf, string fileFilters);
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="dirPath"></param>
        /// <param name="listOf"></param>
        /// <param name="fileFilters"></param>
        /// <returns></returns>
        public abstract IBizObjList getImages(string dirPath,ImageListType listOf, string fileFilters, DateTime changedSince);
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="baseLocation"></param>
        /// <returns></returns>
        public abstract IBizObjList getLocations(string baseLocation);
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
		public virtual bool deleteImage(string name)
		{
			throw new ImplementationDisabledException();
		}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="img"></param>
        /// <returns></returns>
		public virtual bool addImage(IBizObj img)
		{
			throw new ImplementationDisabledException();
		}

		public virtual bool moveImage(string name, string newLocation)
		{
			throw new ImplementationDisabledException();
		}

        public abstract bool hasImageWithName(string imgName);

        protected abstract string getOrigionalImageFileNameExt(string imageFileName);
        
		protected abstract bool isScaledImageOutdated(ref string origionalName, IBizObj scaleOptions, IBizObj imgAdjInfo);
        
        protected abstract MediaStream retrieveImageStream(string name, IBizObj scaleOptions, IBizObj imgAdjInfo);
        
        protected abstract void saveImage(Bitmap image, string name, IBizObj scaleOptions, IBizObj imgAdjInfo, EncoderParameters eps);
        
        protected abstract IBizObj retrieveImage(string name, IBizObj scaleOptions, IBizObj imgAdjInfo);
        
		protected ICrypto myCrypto
		{
			get
			{
				if(m_crypto == null)
				{
					string cryptoName = (string)m_cacheOptions.get("cryptoName");
					if(cryptoName != null)
					{
						ICryptoManager cmgr = (ICryptoManager)Application.currentApp.appData.get("sysObjs-Crypto");
						m_crypto = cmgr;
					}
				}
				return m_crypto;
			}
		}
        
		protected IBizObj getScalingOptions()
		{
            string scalingName = "";
            IBizObj user = Application.currentApp.securityMgr.currentPrincipal.additionalInfo as IBizObj;
            if (user != null && user.containsField("settings.imageScalingName"))
                scalingName = user.get("settings.imageScalingName").ToString();

			if (Cmn.IsEmpty(scalingName))
				scalingName = "[0]";
            IBizObj scalingOptions = m_cacheOptions.get("scalings." + scalingName) as IBizObj;
			if(scalingOptions == null)
                throw new MediaCacheException("Unknown size for this cache: " + scalingName);
			return scalingOptions;
		}
        
		protected string calcRelativeName(string origionalName, IBizObj options)
		{
            if (ExtensionName != "" && origionalName.EndsWith(ExtensionName))
                origionalName = origionalName.Substring(0, origionalName.Length - ExtensionName.Length -1);
            return origionalName;
		}
        
		protected virtual Bitmap retrieveImageBitmap(string name, IBizObj scaleOptions, IBizObj imgAdjInfo)
		{
			Stream str = retrieveImageStream(name, scaleOptions, imgAdjInfo).DataStream;
			return (Bitmap)Bitmap.FromStream(str, true);
		}
        
		protected void createScaledImage(string origionalName, IBizObj sizing, IBizObj imgAdjInfo)
		{
			int maxAttempts = 3;
			int curAttempt = 0;
			while(++curAttempt <= maxAttempts)
			{
                Binary bin = retrieveImageStream(origionalName, null, null);
				if (bin == null)
					return;
                try
                {
                    Bitmap origImage = (Bitmap)Bitmap.FromStream(bin.DataStream, false, false);


                    int desiredWidth = ((int)imgAdjInfo.get("width") <= 0) ? 0 : (int)imgAdjInfo.get("width");
                    int desiredHeight = ((int)imgAdjInfo.get("height") <= 0) ? 0 : (int)imgAdjInfo.get("height");

                    int width = desiredWidth;
                    int height = desiredHeight;
                    if ((bool)imgAdjInfo.get("maintainAspectRatio"))
                    {
                        double heightRatio = (double)origImage.Height / origImage.Width;
                        double widthRatio = (double)origImage.Width / origImage.Height;

						if (width != 0)
						{
							width = desiredWidth;
							height = Convert.ToInt32(width * heightRatio);
						}
						else
						{
							height = desiredHeight;
							width = Convert.ToInt32(height * widthRatio);
						}
                    }

					//Bitmap newImage = new Bitmap(width, height);
					//Graphics g = Graphics.FromImage(newImage);
					//g.FillRectangle(Brushes.White, new Rectangle(0, 0, width, height));
					//g.CompositingQuality = CompositingQuality.GammaCorrected;
					//g.InterpolationMode = InterpolationMode.HighQualityBicubic;
					//g.SmoothingMode = SmoothingMode.HighQuality;
					//g.DrawImage(origImage, 0,0, width, height);

					Bitmap newImage = new Bitmap(width, height);
					Graphics g = Graphics.FromImage(newImage);
                    
					//TODO: Make background fill an option for image adjustment. They can spec color.
					//g.FillRectangle(Brushes.White, new Rectangle(0, 0, width, height));
					g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;

					g.DrawImage(origImage, 0, 0, width, height);

                    if (origImage.Palette != null && origImage.Palette.Entries.Length > 0)
                        newImage.Palette = origImage.Palette;

                    if (((string)imgAdjInfo.get("watermarkText")) != "")
                        addWatermarkText(newImage, g, imgAdjInfo);
                    string wi = ((string)imgAdjInfo.get("watermarkImage"));
                    if (((string)imgAdjInfo.get("watermarkImage")) != "")
                        addWatermarkImage(newImage, g, imgAdjInfo);

                    EncoderParameters eps = new EncoderParameters(1);
                    eps = new EncoderParameters();
                    eps.Param[0] = new EncoderParameter(Encoder.Quality, (long)(int)imgAdjInfo.get("quality"));

                    saveImage(newImage, origionalName, sizing, imgAdjInfo, eps);

                    g.Dispose();
                    newImage.Dispose();
                    origImage.Dispose();
                    eps.Dispose();
                    return;
                }
                catch { }
                finally
                {
                    bin.Dispose();
                }
			}
		}
        
		protected virtual System.Drawing.Imaging.ImageCodecInfo getDesiredEncoder(string imageSubType) 
		{
            if (string.Compare("gif", imageSubType, true) == 0)
                imageSubType = "png";
			return getEncoderInfo(MediaStream.calcMimeType(imageSubType));
		}

		/// <summary>
		/// Gets the encoder information for the specified mimetype.  Used in imagescaling
		/// </summary>
		/// <param name="mimeType">The mimetype of the picture.</param>
		/// <returns>System.Drawing.Imaging.ImageCodecInfo</returns>
		public static System.Drawing.Imaging.ImageCodecInfo getEncoderInfo(string mimeType)
		{
			System.Drawing.Imaging.ImageCodecInfo [] myEncoders =
				System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders();

			foreach (System.Drawing.Imaging.ImageCodecInfo myEncoder in myEncoders)
				if (myEncoder.MimeType == mimeType)
					return myEncoder;
			return null;
		}

		/// <summary>
		/// Will modify the given Bitmap by adding semi-transparent text from the sizing options
		/// over the image.  The result is the origional image with a "watermark" in order to help
		/// identify something about it.
		/// </summary>
		/// <param name="img"></param>
		/// <param name="imgG"></param>
		/// <param name="sizing"></param>
		protected void addWatermarkText(Bitmap img, Graphics imgG, IBizObj sizing)
		{
			int[] sizes = new int[]{16,14,12,10,8,6,4};
			Font crFont = null;
			SizeF crSize = new	SizeF();
			for (int i=0 ;i<sizes.Length; ++i)
			{
				crFont = new Font("arial", sizes[i], FontStyle.Bold);
				crSize = imgG.MeasureString((string)sizing.get("watermarkText"), crFont);

				if((ushort)crSize.Width < (ushort)img.Width)
					break;
			}

			float xpos = 0;
			float ypos = 0;

			WatermarkPosition pos = (WatermarkPosition)Enum.
				Parse(typeof(WatermarkPosition), (string)sizing.get("watermarkPos"), true);
			switch(pos)
			{
				case WatermarkPosition.topLeft:
					xpos = ((float)img.Width * (float).01) + (crSize.Width / 2);
					ypos = (float)img.Height * (float).01;
					break;
				case WatermarkPosition.topRight:
					xpos = ((float)img.Width * (float).99) - (crSize.Width / 2);
					ypos = (float)img.Height * (float).01;
					break;
				case WatermarkPosition.bottomRight:
					xpos = ((float)img.Width * (float).99) - (crSize.Width / 2);
					ypos = ((float)img.Height * (float).99) - crSize.Height;
					break;
				case WatermarkPosition.bottomLeft:
					xpos = ((float)img.Width * (float).01) + (crSize.Width / 2);
					ypos = ((float)img.Height * (float).99) - crSize.Height;
					break;
			}

			StringFormat StrFormat = new StringFormat();
			StrFormat.Alignment = StringAlignment.Center;

			SolidBrush semiTransBrush2 = new SolidBrush(Color.FromArgb(153, 0, 0,0));
			imgG.DrawString((string)sizing.get("watermarkText"), crFont, semiTransBrush2, xpos+1, ypos+1, StrFormat);

			SolidBrush semiTransBrush = new SolidBrush(Color.FromArgb(153, 255, 255, 255));
			imgG.DrawString((string)sizing.get("watermarkText"), crFont, semiTransBrush, xpos, ypos, StrFormat);


			semiTransBrush2.Dispose();
			semiTransBrush.Dispose();
		}

		/// <summary>
		/// With modify the given Bitmap by adding a semi-transparent image from the sizing
		/// options over it.  The result is the origional image with a "watermark" to help
		/// identify something about it.
		/// </summary>
		/// <param name="img"></param>
		/// <param name="imgG"></param>
		/// <param name="sizing"></param>
		protected void addWatermarkImage(Bitmap img, Graphics imgG, IBizObj sizing)
		{
			Image watermark = new Bitmap((string)sizing.get("watermarkImage"));

			ImageAttributes imageAttributes = new ImageAttributes();
			ColorMap colorMap = new ColorMap();

			colorMap.OldColor = Color.FromArgb(255, 0, 255, 0);
			colorMap.NewColor = Color.FromArgb(0, 0, 0, 0);
			ColorMap[] remapTable = {colorMap};

			imageAttributes.SetRemapTable(remapTable, ColorAdjustType.Bitmap);

			float[][] colorMatrixElements = {
												new float[] {1.0f,  0.0f,  0.0f,  0.0f, 0.0f},
												new float[] {0.0f,  1.0f,  0.0f,  0.0f, 0.0f},
												new float[] {0.0f,  0.0f,  1.0f,  0.0f, 0.0f},
												new float[] {0.0f,  0.0f,  0.0f,  0.3f, 0.0f},
												new float[] {0.0f,  0.0f,  0.0f,  0.0f, 1.0f}
											};

			ColorMatrix colorMatrix = new ColorMatrix(colorMatrixElements);

			imageAttributes.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, 
				ColorAdjustType.Bitmap);

			int xpos = 0;
			int ypos = 0;

			WatermarkPosition pos = (WatermarkPosition)Enum.
				Parse(typeof(WatermarkPosition), (string)sizing.get("watermarkPos"), true);
			switch(pos)
			{
				case WatermarkPosition.topLeft:
					xpos = 10;
					ypos = 10;
					break;
				case WatermarkPosition.topRight:
					xpos = ((img.Width - watermark.Width) - 10);
					ypos = 10;
					break;
				case WatermarkPosition.bottomRight:
					xpos = ((img.Width - watermark.Width) - 10);
					ypos = img.Height - watermark.Height - 10;
					break;
				case WatermarkPosition.bottomLeft:
					xpos = 10;
					ypos = img.Height - watermark.Height - 10;
					break;
			}

			imgG.DrawImage(watermark, new Rectangle(xpos, ypos, watermark.Width, watermark.Height), 
				0, 0, watermark.Width, watermark.Height, GraphicsUnit.Pixel, imageAttributes);

			watermark.Dispose();
			imageAttributes.Dispose();
		}

		#region INameAddressSupport Members
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="nameAddress"></param>
        /// <returns></returns>
		public bool containsField(string nameAddress)
		{
			return m_cacheOptions.containsField(nameAddress);
		}

        /// <summary>
        /// 
        /// </summary>
		public bool isGetOnly {get{return true;}}
        
        /// <summary>
        /// 
        /// </summary>
		public XmlSerializationSupport SerializationSupport {get{return m_cacheOptions.SerializationSupport;}}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="nameAddress"></param>
        /// <param name="val"></param>
        /// <param name="extraInfo"></param>
		public void put(string nameAddress, object val, object extraInfo)
		{/*/Intentionally left blank.  This is a read only INameAddressSupport*/}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="nameAddress"></param>
        /// <param name="val"></param>
		public void put(string nameAddress, object val)
		{/*/Intentionally left blank.  This is a read only INameAddressSupport*/}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="options"></param>
        /// <returns></returns>
		public string getXml(string options)
		{
			return m_cacheOptions.getXml(options);
		}
        
        /// <summary>
        /// 
        /// </summary>
		public string Xml {get{return m_cacheOptions.Xml;}}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="nameAddress"></param>
        /// <returns></returns>
		public virtual object get(string nameAddress)
		{
			if(nameAddress == null || nameAddress == "") return this;
			return get(nameAddress, ImageInfoType.asRelativeName);
		}
        
		protected static char[] s_divider = new char[] {'/','\\'};
		
        /// <summary>
		/// Gets an image or this object based on the values in nameAddress and oneArg
		/// </summary>
		/// <remarks>When an image name has embedded folder names in it, the first
		/// folder cannot be named the same as a sizing option.  For example, if the
		/// base image directory is images and a requested image is in Thumbnail\MyPic, this
		/// method will not be able to differentiate between a Thumbnail directory for the 
		/// MyPic image or a Thumbnail sizing option.  Other then this, the image can be
		/// in a very deep folder.  For example "folder1\folder2\folder3\MyPic.Jpg"
		/// can be retrieved as a thumbnail with "thumbnail\folder1\folder2\folder3\MyPic"
		/// and a medium sized one with "medium\folder1\folder2\folder3\MyPic"</remarks>
		/// <param name="nameAddress"></param>
		/// <param name="oneArg">Can be a string of the ImageInfoType enum or an actual ImageInfoType instance.
		/// This guids what is returned from this method: A url of the image request,
		/// A System.Drawing.Image instance, or a stream of the image suitable to pass
		/// in a web response or to System.Drawing.Image.FromStream.</param>
		/// <returns>An image object, image url, or this object.</returns>
		public virtual object get(string nameAddress, object oneArg)
		{
			//When the nameAddress doesn't contain a name of a media file, then this
			//object gets returned.
			if(nameAddress == null) return this;

			//Because this is a leaf node in the application object model, there is never
			//a chance part of the nameAddress is to drill down to children.  Periods in
			//the nameAddress are now considered a part of the image name from the cache.
			ImageInfoType returnType = ImageInfoType.asRelativeName;
			string name = nameAddress;
			string scalingInfo = null;

			//See if a scaling name was prefixed onto the name of the image.  If the first
			//dir name of the image file name is a valid scaling option for this image
			//cache then it is striped from the file name and used as the scaling option.
			int pos = nameAddress.IndexOfAny(s_divider);
			if(pos != -1)
			{
				scalingInfo = nameAddress.Substring(0, pos);
                IBizObj scaling = getScalingOptions();
				if(scaling.get("adjustments." + scalingInfo) == null)
					scalingInfo = null;
				else
					name = nameAddress.Substring(pos +1);
			}

			try
			{
				if(oneArg is string)
					returnType = (ImageInfoType)Enum.Parse(typeof(ImageInfoType), (string)oneArg, true);
				else if(oneArg is ImageInfoType)
					returnType = (ImageInfoType)oneArg;
			}
			catch(Exception e)
			{
				throw new MediaCacheException("Unable to parse requested image return type:" + oneArg.ToString(), e);
			}

			switch(returnType)
			{
				case ImageInfoType.asImageObj:
					return getImageBitmap(name, scalingInfo);
				case ImageInfoType.asMediaStream:
					return getImageStream(name, scalingInfo);
			}
			return name;
		}
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="nameAddress"></param>
        /// <param name="infoArgs"></param>
        /// <returns></returns>
		public virtual object get(string nameAddress, params object[] infoArgs)
		{
			if(nameAddress == null) return this;

			if(infoArgs.Length > 0)
				return get(nameAddress, infoArgs[0]);
			else 
				return get(nameAddress);
		}
		#endregion
		public System.Collections.IEnumerator GetEnumerator() { return null; }
	}

    public enum WatermarkPosition { topLeft, topRight, bottomLeft, bottomRight, center }
    public enum ImageInfoType { asRelativeName, asImageObj, asMediaStream };
    public enum ImageListType
    {
        /// <summary>
        /// Gets only the files that match the pattern passed from the base image sorce directory.
        /// Child directories are simply ignored.
        /// </summary>
        filesOnly,
        /// <summary>
        /// Similar to the filesOnly option, but includes directories as though they were images in
        /// the source directory.
        /// </summary>
        filesWithDirsAsImages,
        /// <summary>
        /// Gets only the files that match the pattern passed from the base image source
        /// directory and all of its child directories recursively.  Every image in all of the
        /// directories within the tree of directories below it are included.  The name of the
        /// image will contain a relative name with the child directories embedded in it. 
        /// </summary>
        deepFiles
    };
}